Challenge 1¶
1. Basics of Image and Signal Processing (LE1)¶
1.1. Image Properties¶
Day 1¶
Find or acquire 1-3 images related to your selected country Mexic0. The images should be suitable for demonstrating adjustments to image properties brightness and hue in experiments.
The pictures are from when i was living in Mexico.
repository at https://github.com/BR4GR/gbsv-challenges
code writen with the help of the Deep Dive repo chatgpt and opencv.org
import cv2 as cv
import librosa
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sounddevice as sd
import time as time1
from ipywidgets import interact, widgets
from matplotlib import pyplot as plt
from matplotlib.widgets import Slider, Button, TextBox
from scipy.io.wavfile import write
jellyfish = cv.imread('img/jellyfish.jpg')
print(f"{jellyfish.shape=}\n{jellyfish.dtype=}\n{np.max(jellyfish)=}\n{np.min(jellyfish)=}")
jellyfish = cv.cvtColor(jellyfish, cv.COLOR_BGR2RGB)
plt.imshow(jellyfish)
plt.show()
cavediving = cv.imread('img/cavediving.jpg')
print(f"{cavediving.shape=}\n{cavediving.dtype=}\n{np.max(cavediving)=}\n{np.min(cavediving)=}")
cavediving = cv.cvtColor(cavediving, cv.COLOR_BGR2RGB)
plt.imshow(cavediving)
plt.show()
sunrise = cv.imread('img/sunrise.jpg')
print(f"{sunrise.shape=}\n{sunrise.dtype=}\n{np.max(sunrise)=}\n{np.min(sunrise)=}")
sunrise = cv.cvtColor(sunrise, cv.COLOR_BGR2RGB)
plt.imshow(sunrise)
plt.show()
jellyfish.shape=(3648, 5472, 3)
jellyfish.dtype=dtype('uint8')
np.max(jellyfish)=np.uint8(255)
np.min(jellyfish)=np.uint8(0)
cavediving.shape=(3648, 5472, 3)
cavediving.dtype=dtype('uint8')
np.max(cavediving)=np.uint8(255)
np.min(cavediving)=np.uint8(0)
sunrise.shape=(4000, 6000, 3)
sunrise.dtype=dtype('uint8')
np.max(sunrise)=np.uint8(255)
np.min(sunrise)=np.uint8(0)
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
# Function to adjust brightness for an RGB image
def adjust_brightness(image, brightness=0):
"""
Adjust the brightness of an RGB image.
Parameters:
- image: Input image in RGB format
- brightness: Amount to adjust brightness (can be positive or negative)
Returns:
- Brightness-adjusted image
"""
image = np.int16(image)
image = image + brightness
image = np.clip(image, 0, 255) # Ensure values stay within [0, 255]
return np.uint8(image)
def adjust_hue(image, hue_shift=0):
"""
Adjust the hue of an RGB image.
Parameters:
- image: Input image in RGB format
- hue_shift: Amount to shift the hue by (in degrees)
Returns:
- Hue-adjusted image
"""
hsv_image = cv.cvtColor(image, cv.COLOR_RGB2HSV)
hue = hsv_image[:, :, 0].astype(int)
hue = (hue + hue_shift) % 180 # Ensure hue stays within the range [0, 179]
hsv_image[:, :, 0] = hue.astype(np.uint8)
return cv.cvtColor(hsv_image, cv.COLOR_HSV2RGB)
def apply_adjustments(image, brightness=0, hue_shift=0):
"""
Apply brightness and hue adjustments to an RGB image.
Parameters:
- image: Input image in RGB format
- brightness: Amount to adjust brightness
- hue_shift: Amount to shift the hue by
Returns:
- Image with brightness and hue adjustments applied
"""
bright_image = adjust_brightness(image, brightness)
final_image = adjust_hue(bright_image, hue_shift)
return final_image
# Function to display the original and adjusted images side by side
def display_images_side_by_side(image, brightness=0, hue_shift=0, ax=None, title="Image"):
"""
Display the original and adjusted images side by side for comparison.
Parameters:
- image: The input image in RGB format
- brightness: Amount to adjust brightness
- hue_shift: Amount to shift the hue by
"""
adjusted_image = apply_adjustments(image, brightness, hue_shift)
# Original image
ax[0].imshow(image)
ax[0].set_title(f'Original {title}')
ax[0].axis('off')
# Adjusted image
ax[1].imshow(adjusted_image)
ax[1].set_title(f'Adjusted {title} (Brightness: {brightness}, Hue: {hue_shift})')
ax[1].axis('off')
def display_all_images():
"""
Display all images with fixed adjustments for brightness and hue.
"""
# Fixed values for brightness and hue for each image
brightness_jellyfish = -40
hue_jellyfish = 40
brightness_cave = -10
hue_cave = 20
brightness_sunrise = 10
hue_sunrise = -20
fig, axes = plt.subplots(3, 2, figsize=(12, 12))
display_images_side_by_side(jellyfish, brightness_jellyfish, hue_jellyfish, axes[0], "jellyfish")
display_images_side_by_side(cavediving, brightness_cave, hue_cave, axes[1], "cave diving")
display_images_side_by_side(sunrise, brightness_sunrise, hue_sunrise, axes[2], "sunrise")
plt.tight_layout()
plt.savefig("img/adjusted_images2x3.png", dpi=300)
plt.show()
display_all_images()
Day 2¶
Define a problem and use case related to your country profile (Steckbrief) for each assigned property that you intend to address (brightness and hue). Then define 1-2 experiments with objectives on how you want to address your defined problem and use case. Multiple experiments/objectives can be analyzed in one single image. Write concisely for each experiment: WHAT problem and use case do you address, HOW do you want to address/solve the problem for this specific use case, and WHY is your experiment helpful for the chosen use case?
Problem:¶
The jellyfish image was taken in a aquarium in playa del Carmen, the room was extreemly dark even thought it was full of different changing colorfull lights, which ilumnated the jelyfish. the image is too bright, and the variety of colors from the light show is not well represented. The dark room and colorful glowing effect of the jellyfish are lost due to overexposure. This results in the background becoming too visible and reduces the visual focus on the jellyfish, which were meant to glow against a dark backdrop.
Use Case:¶
Simulating the original light show where jellyfish glowed in different colors in a dark room. The objective is to recreate the low-light ambiance and simulate the various colors the jellyfish displayed during the light show using hue adjustments.
Objectives of the Experiments:¶
Brightness Adjustment: Reduce the overall brightness to restore the dark environment, ensuring that the jellyfish glow remains the visual focus. How: Lower the brightness to darken the background while keeping the jellyfish visible, mimicking the darkroom atmosphere. we will test and compare different methodes of brightness ajustment, like gamma, region speciffic brightness, or clahe.
Hue Adjustment:* Simulate the changing colors of the light show by adjusting the hue. This allows us to create the effect of different colored lights shining on the jellyfish, even though the image only shows one color. How: Shift the hue to enhance the jellyfish’s glow and replicate the dynamic color changes seen during the light show.
Explanation, Why are these adjustments relevant?¶
Brightness: The dark room is an essential part of the jellyfish exhibit, where overexposure distorts the intended visual experience. Reducing brightness restores this intended ambiance.
Hue: The light show was dynamic and colorful, and adjusting the hue allows us to simulate the color transitions that were key to the experience. This makes the image more visually accurate and engaging.
Day 3¶
Conduct part 1 of your experiments with your images and suitable methods. Reason your choice of parameters and methods using visualizations or 1-2 sentences emach.
def adjust_gamma(image, gamma=1.0):
"""
Adjust the gamma correction of an image.
Parameters:
- image: Input image in RGB format
- gamma: Gamma value for correction (default is 1.0)
Returns:
- Gamma-corrected image
"""
inv_gamma = 1.0 / gamma
table = np.array([(i / 255.0) ** inv_gamma * 255 for i in np.arange(0, 256)]).astype("uint8")
return cv.LUT(image, table)
def apply_region_specific_brightness(image, brightness_value):
"""
Apply region-specific brightness adjustment to an image.
Parameters:
- image: Input image in RGB format
- brightness_value: Amount to adjust brightness (can be positive or negative)
Returns:
- Image with region-specific brightness adjustment applied
"""
mask = image[:, :, 2] < 100 # Apply brightness adjustment to the darker regions only
brightened_image = adjust_brightness(image.copy(), brightness_value)
image[mask] = brightened_image[mask]
return image
def apply_clahe(image, clip_limit=2.0, tile_grid_size=(8, 8)):
"""
Apply Contrast Limited Adaptive Histogram Equalization (CLAHE) to an image.
Parameters:
- image: Input image in RGB format
- clip_limit: Threshold for contrast limiting
- tile_grid_size: Size of grid for histogram equalization
Returns:
- Image with CLAHE applied
"""
lab_image = cv.cvtColor(image, cv.COLOR_RGB2LAB)
clahe = cv.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size)
lab_image[:, :, 0] = clahe.apply(lab_image[:, :, 0])
return cv.cvtColor(lab_image, cv.COLOR_LAB2RGB)
def apply_vignette(image, radius_factor=2, brightness_reduction=0.5):
"""
Apply a vignette effect to an image.
Parameters:
- image: Input image in RGB format
- radius_factor: Factor to control the size of the vignette
- brightness_reduction: Amount to reduce brightness at the edges
Returns:
- Image with vignette effect applied
"""
rows, cols = image.shape[:2]
center_x, center_y = cols // 2, rows // 2
# Create a Gaussian kernel that will serve as the vignette mask
# Calculate the maximum possible radius
max_radius = np.sqrt(center_x ** 2 + center_y ** 2) / radius_factor
# Create meshgrid for the X and Y coordinates
x = np.arange(cols)
y = np.arange(rows)
X, Y = np.meshgrid(x, y)
# Calculate the distance from the center of the image
distance_from_center = np.sqrt((X - center_x) ** 2 + (Y - center_y) ** 2)
# Normalize the distances to a range between 0 and 1, then reverse (1 at center, 0 at edges)
vignette_mask = np.clip(1 - (distance_from_center / max_radius), 0, 1)
# Apply brightness reduction (e.g., 0.5 means reduce brightness by 50% at the edges)
vignette_mask = vignette_mask ** brightness_reduction # Adjust the strength of reduction
# Apply the vignette mask to each channel (RGB)
vignette_mask_3d = vignette_mask[:, :, np.newaxis] # Make mask compatible with 3-channel image
vignette_image = np.uint8(image * vignette_mask_3d)
return vignette_image
# Experiment: Create three levels of brightness adjustment (80%, 60%, 40%)
brightness_levels = [-15, -30, -45]
gamma_values = [0.85, 0.7, 0.55]
vignette_values = [0.85, 0.7, 0.55]
clahe_clip_limits = [1.0, 2.0, 3.0]
# Create a 3x3 grid for each method and level of adjustment
fig, ax = plt.subplots(5, 3, figsize=(15, 13))
# Row 1: Linear brightness adjustment
for i, brightness in enumerate(brightness_levels):
adjusted_image = adjust_brightness(jellyfish.copy(), brightness)
ax[0, i].imshow(adjusted_image)
ax[0, i].set_title(f'Linear Brightness: {100 + brightness}%')
ax[0, i].axis('off')
# Row 3: Region-specific brightness adjustment
for i, brightness in enumerate(brightness_levels):
region_adjusted_image = apply_region_specific_brightness(jellyfish.copy(), brightness)
ax[1, i].imshow(region_adjusted_image)
ax[1, i].set_title(f'Region-Specific: {100 + brightness}%')
ax[1, i].axis('off')
# Row 2: Gamma correction
for i, gamma in enumerate(gamma_values):
gamma_corrected_image = adjust_gamma(jellyfish.copy(), gamma)
ax[2, i].imshow(gamma_corrected_image)
ax[2, i].set_title(f'Gamma Correction: {gamma}')
ax[2, i].axis('off')
# Row 4: CLAHE with different clip limits (strengths)
for i, clip_limit in enumerate(clahe_clip_limits):
clahe_image = apply_clahe(jellyfish.copy(), clip_limit=clip_limit)
ax[3, i].imshow(clahe_image)
ax[3, i].set_title(f'CLAHE: Clip Limit = {clip_limit}')
ax[3, i].axis('off')
# Row 1: Linear brightness adjustment
for i, brightness in enumerate(vignette_values):
adjusted_image = apply_vignette(jellyfish.copy(), 1, brightness)
ax[4, i].imshow(adjusted_image)
ax[4, i].set_title(f'vignette Correction: {brightness}')
ax[4, i].axis('off')
plt.tight_layout()
plt.savefig("img/brightnessexperiment.png", dpi=300)
plt.show()
Reasoning for Methods and Parameters:¶
Gamma Correction was chosen for its ability to apply non-linear adjustments, which better handles darkening shadows while preserving highlights, making it the best method for enhancing the contrast between the glowing jellyfish and the dark background. CLAHE was chosen for localized contrast enhancement, particularly in low-contrast areas (background). Region-specific adjustments were used to target specific areas like the background, maintaining the focus on the jellyfish. The vignette effect was used as a creative approach to enhance the vintage feel and control the outer brightness. Parameters were chosen by trial and error, using large and small stept untill a godd step size was found for each Method.
Observations:¶
The best result was achieved with Gamma Correction (0.55), which effectively darkened the background while preserving the vibrant, glowing jellyfish. The vignette effect added a vintage feel but wasn't the best fit for the goal of simulating the aquarium light show.
dark_adjusted_jellyfish = adjust_gamma(jellyfish.copy(), 0.55)
# Display the original and adjusted images side by side
fig, ax = plt.subplots(1, 2, figsize=(12, 6))
ax[0].imshow(jellyfish)
ax[0].set_title('Original Jellyfish')
ax[0].axis('off')
ax[1].imshow(dark_adjusted_jellyfish)
ax[1].set_title('Adjusted Jellyfish: Gamma = 0.55')
ax[1].axis('off')
cv.imwrite("img/dark_adjusted_jellyfish.png", cv.cvtColor(dark_adjusted_jellyfish, cv.COLOR_RGB2BGR))
plt.tight_layout()
plt.savefig("img/dark_adjusted_jellyfish_comparison.png", dpi=300)
plt.show()
Day 4¶
Conduct part 2 of your experiments with your images and suitable methods. Reason your choice of parameters and methods using visualizations or 1-2 sentences each.
hue_shifts = [-80, -60, -40, -20, 0, 20, 40, 60, 80]
# Create a 3x3 grid for hue adjustments
fig, ax = plt.subplots(3, 3, figsize=(12, 12))
for i, hue_shift in enumerate(hue_shifts):
hue_adjusted_image = adjust_hue(dark_adjusted_jellyfish.copy(), hue_shift=hue_shift)
row = i // 3
col = i % 3
ax[row, col].imshow(hue_adjusted_image)
ax[row, col].set_title(f'Hue Shift: {hue_shift}°')
ax[row, col].axis('off')
plt.tight_layout()
plt.savefig("img/hue_experiment.png", dpi=300)
plt.show()
# Apply hue shifts to different quadrants of the image
def apply_quadrant_hue_shifts(image, hue_shifts):
"""
Apply different hue shifts to each quadrant of the image.
Parameters:
- image: Input image in RGB format
- hue_shifts: List of hue shifts for each quadrant (in degrees)
"""
# Define the quadrants
#jellyfish.shape is (3648, 5472, 3)
rowsplit = 2000
columnsplit = 3150
top_left = image[0:rowsplit, 0:columnsplit]
top_right = image[0:rowsplit, columnsplit:]
bottom_right = image[rowsplit:, columnsplit:]
bottom_left = image[rowsplit:, 0:columnsplit]
# Apply hue shifts to each quadrant
top_left = adjust_hue(top_left, hue_shifts[0])
top_right = adjust_hue(top_right, hue_shifts[1])
bottom_right = adjust_hue(bottom_right, hue_shifts[2])
bottom_left = adjust_hue(bottom_left, hue_shifts[3])
# Reconstruct the full image with the adjusted quadrants
top_half = np.hstack((top_left, top_right))
bottom_half = np.hstack((bottom_left, bottom_right))
adjusted_image = np.vstack((top_half, bottom_half))
filename = f"img/quadrant_hue_shifts{hue_shifts[0]}_{hue_shifts[1]}_{hue_shifts[2]}_{hue_shifts[3]}.png"
cv.imwrite(filename, cv.cvtColor(adjusted_image, cv.COLOR_RGB2BGR))
print(f"Image saved as {filename}")
apply_quadrant_hue_shifts(dark_adjusted_jellyfish.copy(), [0, 40, 80, 120])
apply_quadrant_hue_shifts(dark_adjusted_jellyfish.copy(), [20, 60, 100, 140])
Image saved as img/quadrant_hue_shifts0_40_80_120.png Image saved as img/quadrant_hue_shifts20_60_100_140.png
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
# Function to apply smooth sliding hue shift from 0° to 180° across the image
def apply_smooth_sliding_hue_shift(image):
"""
Apply a smooth sliding hue shift from 0° to 180° across the image, left to right.
Parameters:
- image: Input RGB image.
Returns:
- Image with a smooth sliding hue shift applied from left to right.
"""
hsv_image = cv.cvtColor(image, cv.COLOR_RGB2HSV)
cols = image.shape[1]
# Create a sliding hue mask based on the column position
for col in range(cols):
# Compute the hue shift smoothly as a proportion of the column position
hue_shift = int((col / cols) * 90) # Scale hue shift between 0 and 90
hsv_image[:, col, 0] = (hsv_image[:, col, 0] + hue_shift) % 180 # Apply the hue shift to the column
# Convert back to RGB
sliding_hue_image = cv.cvtColor(hsv_image, cv.COLOR_HSV2RGB)
return sliding_hue_image
smooth_sliding_hue_image = apply_smooth_sliding_hue_shift(dark_adjusted_jellyfish.copy())
cv.imwrite("img/smooth_sliding_hue_shift_0_to_90.png", cv.cvtColor(smooth_sliding_hue_image, cv.COLOR_RGB2BGR))
True

Reasoning for Methods and Parameters:¶
The uniform hue shifts were chosen to clearly visualize how different color adjustments affect the overall image. This method allowed for a straightforward comparison of how each hue shift impacts the brightness and color of the jellyfish.
The quadrant-based hue shift and th sliding shift was chosen mostly to see what it looks like, and because the different effects are closer together without whitespaces in between. the parameters where chosen to display the biggest amont of differnt shifts, as there was not really atarget shift. in the show there where also all colors present.
Observations:¶
The results resemble the effect in real live better than i expected, it pretty much looks exactly how i remember it. The gif looks way worse than expected the problem is gif uses les color.
# Function to resize image to a smaller resolution using Lanczos interpolation
def resize_image(image, width=1000):
"""
Resize the image while maintaining the aspect ratio, using Lanczos interpolation for better color quality.
Parameters:
- image: Input image in RGB format
- width: The desired width of the output image
Returns:
- Resized image with improved interpolation
"""
height = int(image.shape[0] * (width / image.shape[1])) # Maintain aspect ratio
# Use Lanczos interpolation for better quality when resizing
return cv.resize(image, (width, height), interpolation=cv.INTER_LANCZOS4)
# Resize the image using the updated function
resized_image = resize_image(dark_adjusted_jellyfish.copy(), width=1000)
# Create frames for the GIF
def generate_hue_shift_frames(image, output_folder, num_frames=30):
"""
Generate frames for a smooth hue shift animation.
Parameters:
- image: Input image in RGB format
- output_folder: Folder to save the frames
- num_frames: Number of frames to generate
Returns:
- List of frame paths
"""
step = 180 // num_frames # Define the hue step size
frames = []
for i in range(0, 180, step):
hue_shifted_image = adjust_hue(image.copy(), i)
frame_path = f"{output_folder}/frame_{i}.png"
cv.imwrite(frame_path, cv.cvtColor(hue_shifted_image, cv.COLOR_RGB2BGR)) # Save frame
frames.append(frame_path)
return frames
# Generate frames for the resized image
frames = generate_hue_shift_frames(resized_image, output_folder='hue_frames', num_frames=60)
from PIL import Image
# Create a GIF from the saved PNG frames with dithering
def create_gif(frames, output_path, duration=100):
images = [Image.open(frame) for frame in frames]
# Save the GIF with dithering enabled to help smooth color transitions
images[0].save(
output_path,
save_all=True,
append_images=images[1:],
duration=duration,
loop=0,
dither=Image.Dither.FLOYDSTEINBERG, # Apply dithering
optimize=False # Disable further color optimization to avoid additional color loss
)
# Create the GIF with dithering to improve color quality
create_gif(frames, output_path='img/hue_shift_animation_dithered.gif', duration=100)
GIF¶

Day 5¶
Measure your results using appropriate methods. Reason your choice of methods and parameters using visualizations or 1-2 sentences. Select specific measurement methods that are particularly suitable for your use case. Analyze the histograms of the original images and, if applicable, during your experiments.
from scipy.stats import skew
from skimage.measure import shannon_entropy
def adjust_brightness(image, brightness=0):
"""
Adjust the brightness of an RGB image.
Parameters:
- image: Input image in RGB format
- brightness: Amount to adjust brightness (can be positive or negative)
Returns:
- Brightness-adjusted image
"""
image = np.int16(image)
image = image + brightness
image = np.clip(image, 0, 255) # Ensure values stay within [0, 255]
return np.uint8(image)
# Function to apply gamma correction
def adjust_gamma(image, gamma=1.0):
"""
Adjust the gamma correction of an image.
Parameters:
- image: Input image in RGB format
- gamma: Gamma value for correction (default is 1.0)
Returns:
- Gamma-corrected image
"""
inv_gamma = 1.0 / gamma
table = np.array([(i / 255.0) ** inv_gamma * 255 for i in np.arange(0, 256)]).astype("uint8")
return cv.LUT(image, table)
# Function to plot histograms for brightness adjustments for multiple methods
def plot_multiple_brightness_histograms_log(original_image, methods_dict):
"""
Plots histograms for the original image and multiple brightness adjustment methods with a logarithmic scale.
Parameters:
- original_image: The original input image.
- methods_dict: A dictionary where keys are method names and values are the corresponding adjusted images.
"""
# Convert original to grayscale
original_gray = cv.cvtColor(original_image, cv.COLOR_RGB2GRAY)
# Number of methods + 1 for the original image
num_methods = len(methods_dict)
plt.figure(figsize=(15, 6))
# Plot original image histogram (with log scale on y-axis)
plt.subplot(1, num_methods + 1, 1)
plt.hist(original_gray.ravel(), bins=256, range=(0, 256), color='gray')
plt.yscale('log') # Set y-axis to logarithmic scale
plt.title('Original Image (Log Scale)')
plt.xlabel('Pixel Intensity')
plt.ylabel('Frequency')
# Plot histograms for each method with log scale
for i, (method_name, adjusted_image) in enumerate(methods_dict.items(), 2):
adjusted_gray = cv.cvtColor(adjusted_image, cv.COLOR_RGB2GRAY)
plt.subplot(1, num_methods + 1, i)
plt.hist(adjusted_gray.ravel(), bins=256, range=(0, 256), color='gray')
plt.yscale('log') # Set y-axis to logarithmic scale
plt.title(f'{method_name} (Log Scale)')
plt.xlabel('Pixel Intensity')
plt.tight_layout()
plt.savefig("brightness_comparison_histograms_log.png", dpi=300)
plt.show()
def calculate_brightness_kpis(image):
"""
Calculate Key Performance Indicators (KPIs) for brightness of an image.
Parameters:
- image: Input image in RGB format
Returns:
- Dictionary of KPIs (Mean, Std Dev, Skewness, Dynamic Range, Entropy)
"""
gray_image = cv.cvtColor(image, cv.COLOR_RGB2GRAY)
# Selected KPIs
mean = np.mean(gray_image)
std_dev = np.std(gray_image)
skewness = skew(gray_image.ravel())
dynamic_range = np.max(gray_image) - np.min(gray_image)
entropy = shannon_entropy(gray_image)
return {
"Mean": mean,
"Std Dev": std_dev,
"Skewness": skewness,
"Dynamic Range": dynamic_range,
"Entropy": entropy
}
# Function to calculate KPIs for multiple methods
def compare_brightness_kpis(original_image, methods_dict):
"""
Compare Key Performance Indicators (KPIs) for brightness of an image across multiple methods.
Parameters:
- original_image: The original input image.
- methods_dict: A dictionary where keys are method names and values are the corresponding adjusted images.
Returns:
- Dictionary of KPIs for the original image and each adjusted image.
"""
print("KPIs for the Original Image:")
original_kpis = calculate_brightness_kpis(original_image)
for kpi_name, value in original_kpis.items():
print(f"{kpi_name}: {value}")
for method_name, adjusted_image in methods_dict.items():
print(f"\nKPIs for {method_name}:")
adjusted_kpis = calculate_brightness_kpis(adjusted_image)
for kpi_name, value in adjusted_kpis.items():
print(f"{kpi_name}: {value}")
# Example images: original and two different brightness-adjusted methods
original_image = jellyfish.copy()
# Apply different adjustments
gamma_fish = adjust_gamma(original_image.copy(), gamma=0.55) # Gamma correction
cv.imwrite("img/gamma_fish.png", cv.cvtColor(gamma_fish, cv.COLOR_RGB2BGR))
dark_fish = adjust_brightness(original_image.copy(), brightness=-60) # Linear brightness adjustment
cv.imwrite("img/dark_fish.png", cv.cvtColor(dark_fish, cv.COLOR_RGB2BGR))
# Step 1: Plot histograms for multiple methods
methods_dict = {
"Gamma Correction (0.55)": gamma_fish,
"Linear Brightness (-60)": dark_fish
}
plot_multiple_brightness_histograms_log(original_image, methods_dict)
# Step 2: Compare KPIs for multiple methods
compare_brightness_kpis(original_image, methods_dict)
KPIs for the Original Image: Mean: 39.41889767163935 Std Dev: 37.269260229864 Skewness: 1.9444142675661276 Dynamic Range: 254 Entropy: 6.587779365349093 KPIs for Gamma Correction (0.55): Mean: 15.993016981988047 Std Dev: 27.93977790955635 Skewness: 3.5483630400647086 Dynamic Range: 255 Entropy: 5.137508582224073 KPIs for Linear Brightness (-60): Mean: 11.016358849597953 Std Dev: 23.555591835182078 Skewness: 3.527971057920704 Dynamic Range: 195 Entropy: 4.078345291811171
Gamma (0.55)¶

Linear Brightness (-60)¶

Why the metrics were chosen: The selected metrics directly address the use case of analyzing brightness adjustments. By using these metrics, we can quantitatively determine how the two adjustment methods (gamma correction and linear brightness adjustment) differ in terms of their effects on the image. Mean and standard deviation were chosen as basic indicators of brightness and contrast. Skewness was chosen to see whether the adjustments cause the image to become more biased towards darker or lighter regions. Dynamic Range was selected to quantify how much detail remains after adjustment. Entropy was chosen to measure how much image information and texture is preserved.
Why specific parameters were chosen: Gamma (0.55) was chosen because it darkens the mid-tones more than the highlights or shadows, which is suitable for the jellyfish image where we want to keep glowing areas intact while darkening the background. Linear Brightness (-60) was chosen to uniformly darken the image, providing a straightforward comparison with the more complex gamma adjustment. Both parameters were found by trial and error.
Day 6¶
Select the best results from your experiments. Summarize observations and interpretations about your results and discuss your findings and results in relation to your problem and use case in approximately 150 words.
Both gamma correction (0.55) and linear brightness adjustment (-60) were applied to simulate a dark environment with glowing jellyfish, similar to the original scene. Upon visual inspection, the gamma correction preserved the overall mid-tone contrast but exaggerated some of the bright spots, especially around the center-left of the image. This caused parts of the image to become overexposed and lose subtle detail. In contrast, the linear brightness adjustment resulted in a more balanced effect where the jellyfish glowed softly, and the bright spots remained controlled, making it a better choice for retaining natural highlights.
From a quantitative standpoint, gamma correction resulted in a higher skewness and slightly higher entropy than linear adjustment. However, entropy alone is not the best indicator of quality in this case, as the linear adjustment kept the bright spots intact and avoided oversaturation, preserving the aesthetic and intended visual effect of the jellyfish glowing in a dark room.
Therefore, linear brightness adjustment is the most suitable method for this specific use case of preserving glowing objects in a dark environment.
Day 7¶
Find or acquire 1-3 signals related to your selected country. The signals should be suitable for demonstrating adjustments to signal properties frequency and smoothing in experiments.
audio, sampling_rate = librosa.load("sound/AztecWhistle.wav")
print(f"{audio=}")
print(f"{audio.dtype=}")
print(f"{audio.shape=}")
print(f"{np.max(audio)=}")
print(f"{np.min(audio)=}")
sd.play(audio, sampling_rate)
audio=array([0.00283813, 0.00253296, 0.00274658, ..., 0. , 0. ,
0. ], dtype=float32)
audio.dtype=dtype('float32')
audio.shape=(121174,)
np.max(audio)=np.float32(0.9999695)
np.min(audio)=np.float32(-0.9477539)
time = np.arange(0, len(audio)) / sampling_rate
plt.figure(figsize=(12, 4))
plt.plot(time, audio)
plt.title("Original Aztec Death Whistle Signal")
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")
plt.show()
The Aztec death whistle is an ancient ceremonial instrument used by the Aztecs, known for producing an intense, eerie sound similar to a human scream. It was often used in rituals and psychological warfare to create fear and panic.
Day 8¶
Define a problem and use case related to your country profile (Steckbrief) for each assigned property (frequency and smoothing) that you intend to address. Then define 1-2 experiments with objectives on how you want to address your defined problem and use case. Multiple experiments/objectives can be analyzed in one single signal. Write concisely for each experiment: WHAT problem and use case do you address, HOW do you want to address/solve the problem for this specific use case, and WHY is your experiment helpful for the chosen use case?
problem and use case
- Understanding why certain sounds (like the Aztec death whistle) cause emotional and physical reactions can provide insights into how sound has been used in warfare or rituals. Analyzing the whistle’s signal can help us understand the acoustic properties that trigger these reactions, and experimenting with modifications can demonstrate how small changes can reduce or enhance these unsettling characteristics.
Experiment 1: Smoothing the Signal Using Low-Pass Filtering
Objective: To smooth the chaotic, sharp transitions in the sound to make the signal less jarring, while observing how smoothing affects the emotional impact of the sound.
Method: We will apply low-pass filters with different cutoff frequencies (e.g., 1kHz, 2kHz) to remove the higher, more chaotic frequencies that contribute to the whistle's harshness. This will result in a smoother, softer version of the sound. After applying the filters, we will compare the original and smoothed signals both visually (waveform analysis) and aurally (listening).
Why This is Helpful: Smoothing reduces the sharp, abrupt changes in the sound, potentially making the sound less aggressive and more palatable. This experiment will help demonstrate how removing high-frequency content impacts the emotional response to the whistle, making it useful for educational or research purposes where the sound's intensity needs to be reduced.
Experiment 2: Dynamic Frequency Shifting (Frequency Modulation)
Objective: To experiment with frequency shifting (modulation) techniques to shift the whistle's frequencies up or down the spectrum and analyze how these shifts alter the sound’s emotional effect.
Method: We will apply frequency modulation by shifting the whistle’s base frequencies up and down by 500 Hz, 1000 Hz, and 2000 Hz. This will allow us to experiment with how the pitch-shifting of frequencies changes the emotional impact of the sound.
Why This is Helpful: By shifting the whistle’s frequency spectrum, we can explore how changes in pitch impact the listener’s perception. The goal is to see whether lowering or raising the frequencies diminishes or enhances the unsettling quality of the sound.
Experiment 3: Temporal Smoothing with Savitzky-Golay Filter
Objective: To smooth the signal over time, reducing the sharp changes in amplitude while maintaining the core characteristics of the whistle’s tone.
Method: We will apply a Savitzky-Golay filter, a popular smoothing algorithm, to smooth the time-domain signal. This will help reduce the abrupt transitions in the sound without affecting its frequency components. We will compare the amplitude envelope before and after smoothing to measure the impact on the sound.
Why This is Helpful: Smoothing the time-domain signal will reduce the jarring effect of sudden loud noises while retaining the whistle's tonal characteristics. This is especially useful for environments where the sound’s intensity needs to be softened while maintaining its core identity.
Day 9¶
Conduct part 1 of your experiments with your signals and suitable methods. Reason your choice of parameters and methods using visualizations or 1-2 sentences each.
import librosa.effects
# Function to shift the frequencies
def shift_frequencies(audio, sampling_rate, shift_in_hz, direction="up"):
"""
Shift the frequencies of an audio signal by a specified amount.
Parameters:
- audio: Input audio signal
- sampling_rate: Sampling rate of the audio signal
- shift_in_hz: Amount to shift the frequencies by (in Hz)
- direction: Direction of the shift ("up" or "down")
Returns:
- Audio signal with shifted frequencies
"""
if direction == "up":
shift_in_steps = 12 * np.log2((shift_in_hz + sampling_rate / 2) / (sampling_rate / 2))
elif direction == "down":
shift_in_steps = -12 * np.log2((shift_in_hz + sampling_rate / 2) / (sampling_rate / 2))
# Apply the pitch shift
shifted_audio = librosa.effects.pitch_shift(audio, sr=sampling_rate, n_steps=shift_in_steps)
return shifted_audio
shifted_1000_up = shift_frequencies(audio, sampling_rate, 1000)
shifted_2000_up = shift_frequencies(audio, sampling_rate, 2000)
shifted_3000_up = shift_frequencies(audio, sampling_rate, 3000)
shifted_1000_down = shift_frequencies(audio, sampling_rate, 1000, direction="down")
shifted_2000_down = shift_frequencies(audio, sampling_rate, 2000, direction="down")
shifted_3000_down = shift_frequencies(audio, sampling_rate, 3000, direction="down")
write('sound/shifted_1000_up.wav', sampling_rate, shifted_1000_up)
write('sound/shifted_2000_up.wav', sampling_rate, shifted_2000_up)
write('sound/shifted_3000_up.wav', sampling_rate, shifted_3000_up)
write('sound/shifted_1000_down.wav', sampling_rate, shifted_1000_down)
write('sound/shifted_2000_down.wav', sampling_rate, shifted_2000_down)
write('sound/shifted_3000_down.wav', sampling_rate, shifted_3000_down)
For the frequency shifting experiment, the method of frequency modulation was chosen because altering the base frequencies of a signal directly impacts how listeners perceive pitch. Frequency modulation allows us to explore whether increasing or decreasing pitch intensifies or reduces the emotional impact, providing insight into the acoustic properties that make the sound disturbing.
The parameters for the frequency shifts were chosen by trial and error.
from IPython.display import Markdown, Audio
# Function to plot a single signal and provide audio playback
def plot_signal_with_audio(audio_signal, title, sampling_rate, audio_label):
plt.figure(figsize=(10, 4))
plt.plot(time, audio_signal)
plt.title(title)
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")
plt.tight_layout()
plt.show()
# Display the audio player with a label
display(Markdown(f"**{audio_label}**"))
display(Audio(audio_signal, rate=sampling_rate))
# Plot the original signal
plot_signal_with_audio(audio, title="Original Signal", sampling_rate=sampling_rate, audio_label="Original Audio Playback")
# Plot +1000 Hz and -1000 Hz shifted signals
plot_signal_with_audio(shifted_1000_up, title="+1000 Hz Shifted Signal", sampling_rate=sampling_rate, audio_label="+1000 Hz Shifted Audio Playback")
plot_signal_with_audio(shifted_1000_down, title="-1000 Hz Shifted Signal", sampling_rate=sampling_rate, audio_label="-1000 Hz Shifted Audio Playback")
# Plot +2000 Hz and -2000 Hz shifted signals
plot_signal_with_audio(shifted_2000_up, title="+2000 Hz Shifted Signal", sampling_rate=sampling_rate, audio_label="+2000 Hz Shifted Audio Playback")
plot_signal_with_audio(shifted_2000_down, title="-2000 Hz Shifted Signal", sampling_rate=sampling_rate, audio_label="-2000 Hz Shifted Audio Playback")
# Plot +3000 Hz and -3000 Hz shifted signals
plot_signal_with_audio(shifted_3000_up, title="+3000 Hz Shifted Signal", sampling_rate=sampling_rate, audio_label="+3000 Hz Shifted Audio Playback")
plot_signal_with_audio(shifted_3000_down, title="-3000 Hz Shifted Signal", sampling_rate=sampling_rate, audio_label="-3000 Hz Shifted Audio Playback")
Original Audio Playback
+1000 Hz Shifted Audio Playback
-1000 Hz Shifted Audio Playback
+2000 Hz Shifted Audio Playback
-2000 Hz Shifted Audio Playback
+3000 Hz Shifted Audio Playback
-3000 Hz Shifted Audio Playback
Day 10¶
Conduct part 2 of your experiments with your signals and suitable methods. Reason your choice of parameters and methods using visualizations or 1-2 sentences each.
from IPython.display import Markdown, Audio
from scipy.signal import butter, filtfilt
# Function to apply low-pass filter
def apply_low_pass_filter(signal, sampling_rate, cutoff_hz):
# Design a butterworth low-pass filter
nyquist = 0.5 * sampling_rate
normal_cutoff = cutoff_hz / nyquist
b, a = butter(N=5, Wn=normal_cutoff, btype='low', analog=False)
filtered_signal = filtfilt(b, a, signal)
return filtered_signal
# Apply low-pass filters with different cutoff frequencies
low_pass_1000 = apply_low_pass_filter(audio, sampling_rate, 1000)
low_pass_2000 = apply_low_pass_filter(audio, sampling_rate, 2000)
# Plot the original and low-pass filtered signals with audio playback
def plot_filtered_signals_with_audio():
plt.figure(figsize=(24, 12))
# Original signal plot
plt.subplot(3, 1, 1)
plt.plot(time, audio)
plt.title("Original Signal")
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")
# Low-pass filtered (1000 Hz) signal plot
plt.subplot(3, 1, 2)
plt.plot(time, low_pass_1000)
plt.title("Low-Pass Filtered Signal (1000 Hz)")
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")
# Low-pass filtered (2000 Hz) signal plot
plt.subplot(3, 1, 3)
plt.plot(time, low_pass_2000)
plt.title("Low-Pass Filtered Signal (2000 Hz)")
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")
plt.tight_layout()
plt.show()
# Display audio players with labels
display(Markdown("**Original Audio Playback**"))
display(Audio(audio, rate=sampling_rate))
display(Markdown("**Low-Pass 1000 Hz Filtered Audio Playback**"))
display(Audio(low_pass_1000, rate=sampling_rate))
display(Markdown("**Low-Pass 2000 Hz Filtered Audio Playback**"))
display(Audio(low_pass_2000, rate=sampling_rate))
# Call the function to plot and display audio
plot_filtered_signals_with_audio()
# Save the filtered audio to files
from scipy.io.wavfile import write
write('sound/low_pass_1000.wav', sampling_rate, low_pass_1000.astype(np.float32))
write('sound/low_pass_2000.wav', sampling_rate, low_pass_2000.astype(np.float32))
Original Audio Playback
Low-Pass 1000 Hz Filtered Audio Playback
Low-Pass 2000 Hz Filtered Audio Playback
sd.play(low_pass_1000, sampling_rate)
sd.play(audio, sampling_rate)
sd.play(low_pass_2000, sampling_rate)
from scipy.signal import savgol_filter
# Apply Savitzky-Golay filter
def apply_savitzky_golay_filter(signal, window_length=101, polyorder=2):
return savgol_filter(signal, window_length=window_length, polyorder=polyorder)
# Apply Savitzky-Golay filter
savitzky_golay_smoothed = apply_savitzky_golay_filter(audio)
# Plot the original and Savitzky-Golay smoothed signals
plt.figure(figsize=(12, 6))
plt.subplot(2, 1, 1)
plt.plot(time, audio)
plt.title("Original Signal")
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")
plt.subplot(2, 1, 2)
plt.plot(time, savitzky_golay_smoothed)
plt.title("Savitzky-Golay Smoothed Signal")
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")
plt.tight_layout()
plt.show()
write('sound/savitzky_golay_smoothed.wav', sampling_rate, savitzky_golay_smoothed.astype(np.float32))
from IPython.display import Markdown, Audio
import librosa.display
# Function to plot spectrogram with audio and label
def plot_spectrogram_with_audio(audio, sampling_rate, title="Spectrogram", audio_label="Audio Playback"):
plt.figure(figsize=(10, 4))
S = librosa.feature.melspectrogram(y=audio, sr=sampling_rate)
S_dB = librosa.power_to_db(S, ref=np.max)
librosa.display.specshow(S_dB, sr=sampling_rate, x_axis='time', y_axis='mel', cmap='magma')
plt.colorbar(format='%+2.0f dB')
plt.title(title)
plt.tight_layout()
# Display the audio label and audio player
display(Markdown(f"**{audio_label}**"))
display(Audio(audio, rate=sampling_rate))
plt.show()
# Example: Plot original and filtered spectrograms with audio playback
plot_spectrogram_with_audio(audio, sampling_rate, title="Original Signal Spectrogram", audio_label="Original Audio Playback")
plot_spectrogram_with_audio(low_pass_1000, sampling_rate, title="Low-Pass Filtered (1kHz) Spectrogram", audio_label="Low-Pass 1000 Hz Audio Playback")
plot_spectrogram_with_audio(low_pass_2000, sampling_rate, title="Low-Pass Filtered (2kHz) Spectrogram", audio_label="Low-Pass 2000 Hz Audio Playback")
plot_spectrogram_with_audio(savitzky_golay_smoothed, sampling_rate, title="Savitzky-Golay Smoothed Spectrogram", audio_label="Savitzky-Golay Smoothed Audio Playback")
Original Audio Playback
Low-Pass 1000 Hz Audio Playback
Low-Pass 2000 Hz Audio Playback
Savitzky-Golay Smoothed Audio Playback
from IPython.display import display, Audio, Markdown
from scipy.signal import hilbert
# Function to plot envelope with audio and label
def plot_envelope_with_audio(audio, sampling_rate, title="Signal Envelope", audio_label="Audio Playback"):
# Apply Hilbert transform to get the envelope
analytic_signal = hilbert(audio)
envelope = np.abs(analytic_signal)
time = np.arange(len(envelope)) / sampling_rate
# Plot the envelope
plt.figure(figsize=(10, 4))
plt.plot(time, envelope)
plt.title(title)
plt.xlabel('Time (s)')
plt.ylabel('Envelope Amplitude')
plt.tight_layout()
plt.show()
# Display a label for the audio
display(Markdown(f"**{audio_label}**"))
# Embed the audio playback directly below the envelope plot
display(Audio(audio, rate=sampling_rate))
# Example: Plot envelopes and add labeled audio playback for original and filtered signals
plot_envelope_with_audio(audio, sampling_rate, title="Original Signal Envelope", audio_label="Original Audio")
plot_envelope_with_audio(low_pass_1000, sampling_rate, title="Low-Pass Filtered (1kHz) Envelope", audio_label="Low-Pass 1000 Hz Audio")
plot_envelope_with_audio(savitzky_golay_smoothed, sampling_rate, title="Savitzky-Golay Smoothed Signal Envelope", audio_label="Savitzky-Golay Smoothed Audio")
Original Audio
Low-Pass 1000 Hz Audio
Savitzky-Golay Smoothed Audio
Day 11¶
Measure your results using appropriate methods. Reason your choice of methods and parameters using visualizations or 1-2 sentences. Select specific measurement methods that are particularly suitable for your use case.
# Function to calculate KPIs and convert to standard Python floats
def calculate_kpis(signal, sampling_rate):
# Convert signal to mono if necessary
signal_mono = librosa.to_mono(signal)
# Calculate standard KPIs
mean = float(np.mean(signal_mono))
std_dev = float(np.std(signal_mono))
skewness = float(skew(signal_mono))
dynamic_range = float(np.max(signal_mono) - np.min(signal_mono))
entropy = float(shannon_entropy(signal_mono))
# Spectral features
spectral_centroid = float(np.mean(librosa.feature.spectral_centroid(y=signal_mono, sr=sampling_rate)))
spectral_bandwidth = float(np.mean(librosa.feature.spectral_bandwidth(y=signal_mono, sr=sampling_rate)))
zero_crossing_rate = float(np.mean(librosa.feature.zero_crossing_rate(signal_mono)))
return {
"Mean": mean,
"Standard Deviation": std_dev,
"Skewness": skewness,
"Dynamic Range": dynamic_range,
"Entropy": entropy,
"Spectral Centroid": spectral_centroid,
"Spectral Bandwidth": spectral_bandwidth,
"Zero Crossing Rate": zero_crossing_rate
}
# Example: Calculate KPIs for original and processed signals
kpis_original = calculate_kpis(audio, sampling_rate)
kpis_shifted_3000 = calculate_kpis(shifted_3000_up, sampling_rate)
kpis_low_pass_1000 = calculate_kpis(low_pass_1000, sampling_rate)
# Prepare the KPI data
kpis_data = {
"Original Signal": kpis_original,
"+3000 Hz Shifted": kpis_shifted_3000,
"Low-Pass 1000 Hz": kpis_low_pass_1000
}
# Convert the KPI data into a DataFrame for a cleaner table display
kpis_df = pd.DataFrame(kpis_data)
# Display the KPIs in table format
print(kpis_df)
Original Signal +3000 Hz Shifted Low-Pass 1000 Hz Mean -0.000004 0.000003 -0.000004 Standard Deviation 0.109932 0.074204 0.014490 Skewness 0.039047 0.038837 -0.079182 Dynamic Range 1.947723 1.586893 0.711466 Entropy 13.321276 16.885730 16.886721 Spectral Centroid 2256.571799 2672.534128 944.300994 Spectral Bandwidth 1413.563726 1437.703945 260.682652 Zero Crossing Rate 0.161120 0.195181 0.085631
import matplotlib.pyplot as plt
import numpy as np
# Function to create bar charts for comparing KPIs
def plot_kpi_comparison(kpis_dict, title):
"""
kpis_dict: Dictionary of KPIs with keys as signal names and values as KPI dictionaries.
"""
metrics = list(kpis_dict[list(kpis_dict.keys())[0]].keys()) # Get list of KPI names
# Create a figure for the bar charts
num_metrics = len(metrics)
fig, axs = plt.subplots(1, num_metrics, figsize=(20, 5))
signals = list(kpis_dict.keys()) # List of signals (Original, Shifted, Filtered)
for i, metric in enumerate(metrics):
values = [kpis_dict[signal][metric] for signal in kpis_dict]
axs[i].bar(signals, values)
axs[i].set_title(metric)
axs[i].set_ylabel('Value')
axs[i].set_xticks(range(len(signals))) # Set the tick positions
axs[i].set_xticklabels(signals, rotation=45) # Set tick labels after setting the positions
fig.suptitle(title)
plt.tight_layout()
plt.show()
# Example: Prepare KPI data for visualization
kpis_data = {
"Original Signal": kpis_original,
"+3000 Hz Shifted": kpis_shifted_3000,
"Low-Pass 1000 Hz": kpis_low_pass_1000
}
# Plot KPI comparisons
plot_kpi_comparison(kpis_data, title="KPI Comparison for Original and Processed Signals")
The methods we used to measure the results try to capture the impact of signal processing techniques like frequency shifting and low-pass filtering. I applied a variety of key performance indicators (KPIs) to analyze the signals both before and after processing. Specifically, i chose:
Mean and Standard Deviation:
- These metrics provide insights into the amplitude distribution and variation of the signal, helping us understand how dynamic or smooth the sound is.
Skewness:
- This metric is useful for detecting any asymmetry in the signal's distribution, revealing any biases introduced by shifting the frequencies. Dynamic Range: This shows how much variation exists between the loudest and softest parts of the signal, which helps in understanding how much "depth" the sound has retained or lost.
Entropy:
- This is a measure of complexity in the signal, capturing the amount of information in both the original and processed signals. Spectral Centroid and Spectral Bandwidth: These frequency-based metrics are critical in measuring how the energy distribution and spread of frequencies shift after processing. They are directly tied to how the perceived tone and timbre of the signal change after frequency shifting and filtering.
Zero Crossing Rate: the parameters "+3000 Hz Shifted" and "Low-Pass 1000 Hz" were chosen because they were the least and most extreme.
- This measures the number of times the signal crosses zero, which helps quantify the noisiness or smoothness of the signal. It’s especially useful in examining how the high-frequency noise has been impacted by low-pass filtering.

